Skip to content

qcom-capsule: inject the OEM root cert with qdte-lite, before signing - #3043

Open
Igor Opaniuk (igoropaniuk) wants to merge 5 commits into
qualcomm-linux:masterfrom
igoropaniuk:feat/qdte-lite-capsule-2456
Open

qcom-capsule: inject the OEM root cert with qdte-lite, before signing#3043
Igor Opaniuk (igoropaniuk) wants to merge 5 commits into
qualcomm-linux:masterfrom
igoropaniuk:feat/qdte-lite-capsule-2456

Conversation

@igoropaniuk

@igoropaniuk Igor Opaniuk (igoropaniuk) commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

The OEM capsule root certificate lives in a device tree embedded in a boot config ELF (xbl_config.elf/uefi_dtbs.elf), and getting it there constrains the whole pipeline. Three things must happen in this order:

  1. Inject QcCapsuleRootCert into the config ELF (xbl_config.elf/uefi_dtbs.elf)
  2. sign the result - editing a DTB inside the ELF invalidates any signature it carried (this is what planned to land in [RFC] qcom-firmware-sign: build-time secure-boot signing of Qualcomm firmware #2598)
  3. build the capsule, which the firmware verifies against the certificate from step 1

qcom-capsule.bbclass can't own step 1: by the time it runs, the boot firmware is deployed and whatever signs it has finished. There's no seam left. This series moves injection into its own recipe between the boot firmware and the capsule, which creates one - it stages the injected ELFs and stops, leaving do_compile/do_deploy for a signing step to sit between. That's what makes the secure-boot work in #2598 able to slot in without rewiring anything.

Injection of the root certification (stored in QcCapsuleRootCert property) is done by qdte-lite instead of a fixed dump/set-property/replace sequence. The class no longer needs to know anything about the container:

target=$(qdte-lite --nogui --input_file "${XBL}" --find_property QcCapsuleRootCert)
qcom-capsule-tool bin-to-hex "${CAPSULE_ROOT_CER}" "${ROOT_INC}"
qdte-lite --nogui --input_file "${XBL}" --output_path "${OUT}" \
    --output_file xbl_config.elf --modify "${target}=@list:${ROOT_INC}"

DTB names are assigned during disassembly - from container metadata in one container, from /compatible in another - so asking the tool that assigns them beats configuring them per machine. XBLCONFIG_DTB and XBLCONFIG_DTB_SECTION go away.

Also the capsule flow leaves meta-arm. It lived there only for edk2-basetools (GenFfs, GenFv, GenerateCapsule.py). cbsp-boot-utilities now has its own equivalents, so the dependency and the reason for the dynamic layer are both gone.

Additionally two bugs are fixed:

  • qcom-capsule.bbclass tested for a literal xbl_config.elf, but QCOM_XBL_CONFIG is xbl_config_kvm.elf on kvm machines like iq-9075-evk - injection was silently skipped there, surfacing much later as a capsule the firmware refuses. Now uses QCOM_XBL_CONFIG throughout.
    CAPSULE_ENTRY_dtb[...] flags were unqualified. Varflags take no part in override resolution, so hamoa's SPINOR destinations applied to every machine, and generate_fvupdate() doesn't catch it. Now guarded on MACHINEOVERRIDES.

Can be build with this config (example for RB3Gen2):

kas build ci/rb3gen2-core-kit.yml:ci/capsule.yml:ci/capsule-test-keys.yml

@igoropaniuk

Copy link
Copy Markdown
Contributor Author

xueqnie could you please take a look, thanks!

@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown

Test run workflow

Test jobs for commit 0fd9daf

qcom-distro
Pass: 11 | Fail: 1 | Total: 12
nodistro
Pass: 10 | Fail: 0 | Total: 10
qcom-distro_linux-qcom-6.18
Pass: 7 | Fail: 1 | Total: 8

@test-reporting-app

test-reporting-app Bot commented Aug 31, 2026

Copy link
Copy Markdown

Test Results

 30 files   -    89   30 suites   - 686   2h 47m 58s ⏱️ - 11h 26m 13s
 13 tests  -   167    8 ✅  -   126  0 💤  -  2  5 ❌  - 39 
269 runs   - 4 328  260 ✅  - 4 184  0 💤  - 59  9 ❌  - 85 

For more details on these failures, see this check.

Results for commit 0fd9daf. ± Comparison against base commit f00721c.

This pull request removes 167 tests.
0_AudioRecord_Config01 ‑ AudioRecord_Config01
0_BT_FW_KMD_Service ‑ BT_FW_KMD_Service
0_GStreamer_Video_Encode_h264_480p ‑ GStreamer_Video_Encode_h264_480p
0_Libcamera_cam ‑ Libcamera_cam
0_Weston_Runtime_Preflight ‑ Weston_Runtime_Preflight
0_hotplug ‑ hotplug
10_AudioRecord ‑ AudioRecord
10_Docker_Kernel_Config ‑ Docker_Kernel_Config
10_Kubernetes_Kernel_Config ‑ Kubernetes_Kernel_Config
10_Logging_Journalctl_Validation ‑ Logging_Journalctl_Validation
…

♻️ This comment has been updated with latest results.

@qcomlnxci

Copy link
Copy Markdown

Test Coral run workflow

Test jobs for commit 0e66a76

  • qcomdistro: multimedia image-prop
    Pass: 35 | Fail: 0 | Others: 1 | Total: 36
  • qcomdistro: multimedia image
    Pass: 9 | Fail: 0 | Total: 9

@xueqnie

xueqnie commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

xueqnie could you please take a look, thanks!

Tested on iq-x7181-evk (hamoa). Build fails at firmware-qcom-oem-cert:do_compile
here is log:
| elf_gen.py: Disassembling elf......... | | elf_gen.py: Disassembled elf info generated in:"/tmp/dtgui_mk08zd05/disassembled_elf_info.json" | Conversion successful! Hex data with header written to /mnt/xueqian/hamoa/meta-qcom/build/tmp/work/iq_x7181_evk-oe-linux/firmware-qcom-oem-cert/1.0/sources/firmware-qcom-oem-cert-1.0/QcFMPRoot.inc | | elf_gen.py: Disassembling elf......... | | elf_gen.py: Disassembled elf info generated in:"/tmp/dtgui_mk08zd05/disassembled_elf_info.json" | python version sys.version_info(major=3, minor=14, micro=7, releaselevel='final', serial=0) | proterty_path: /sw/uefi/uefiplat/QcCapsuleRootCert | property_value: [934, 813826978, ...] | dtb_name: qcom,uefi-hamoa-1.0.dtb | pylibfdt error -3: FDT_ERR_NOSPACE in config image | WARNING: .../run.do_compile.3965865:200 exit 255 from 'qdte-lite --nogui \ | --input_file "${config_elf}" --output_path "${outdir}" \ | --output_file "${stem}.elf" --modify "${modify_arg}"' | WARNING: Backtrace (BB generated script): | #1: patch_config_elf_cert, ...run.do_compile.3965865, line 200 | #2: do_compile, ...run.do_compile.3965865, line 150 | #3: main, ...run.do_compile.3965865, line 223 ERROR: Task (...firmware-qcom-oem-cert_1.0.bb:do_compile) failed with exit code '1'

@igoropaniuk

Copy link
Copy Markdown
Contributor Author

xueqnie

Thanks for testing this on hamoa. I haven't been able to reproduce it, so I'd like to narrow down what's different in your setup.

I built firmware-qcom-oem-cert for iq-x7181-evk at this PR's HEAD (0e66a76) and it completes, injecting into both targets and deploying uefi_dtbs-with-oem-cert.xz:

qcom,uefi-hamoa-1.0.dtb/sw/uefi/uefiplat/QcCapsuleRootCert
qcom,uefi-purwa-1.0.dtbo/fragment@6/__overlay__/uefi/uefiplat/QcCapsuleRootCert

I also ran the failing --modify directly, using the qdte-lite from the build's native sysroot (so v2.0.0, oe-core's pylibfdt 1.8.1, native Python 3.14.7 - matching the 3.14.7 in your log) against the stock HAMOA_bootbinaries_00015 container, with the same 934-byte test cert. It succeeds.

Things I checked and ruled out:

  • property growth overflowing the FDT buffer (grew a 1-cell property to 235 cells, and separately to 262 KB, both fine)
  • the two-target & loop and the reload between targets
  • pylibfdt 1.7.2 vs oe-core's 1.8.1

So the remaining variable looks like the input. Could you post:

  1. sha256sum of uefi_dtbs.xz in deploy/images/iq-x7181-evk/iq-x7181/spinor/ - mine is 44984c3b87058cec9c71…
  2. the output of (please install it from https://github.com/qualcomm/qdte-lite):
qdte-lite --nogui --input_file <that uefi_dtbs.xz> --find_property QcCapsuleRootCert
  1. your python3-dtc version / oe-core revision

@xueqnie

xueqnie commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

xueqnie

Thanks for testing this on hamoa. I haven't been able to reproduce it, so I'd like to narrow down what's different in your setup.

I built firmware-qcom-oem-cert for iq-x7181-evk at this PR's HEAD (0e66a76) and it completes, injecting into both targets and deploying uefi_dtbs-with-oem-cert.xz:

qcom,uefi-hamoa-1.0.dtb/sw/uefi/uefiplat/QcCapsuleRootCert
qcom,uefi-purwa-1.0.dtbo/fragment@6/__overlay__/uefi/uefiplat/QcCapsuleRootCert

I also ran the failing --modify directly, using the qdte-lite from the build's native sysroot (so v2.0.0, oe-core's pylibfdt 1.8.1, native Python 3.14.7 - matching the 3.14.7 in your log) against the stock HAMOA_bootbinaries_00015 container, with the same 934-byte test cert. It succeeds.

Things I checked and ruled out:

  • property growth overflowing the FDT buffer (grew a 1-cell property to 235 cells, and separately to 262 KB, both fine)
  • the two-target & loop and the reload between targets
  • pylibfdt 1.7.2 vs oe-core's 1.8.1

So the remaining variable looks like the input. Could you post:

  1. sha256sum of uefi_dtbs.xz in deploy/images/iq-x7181-evk/iq-x7181/spinor/ - mine is 44984c3b87058cec9c71…
  2. the output of (please install it from https://github.com/qualcomm/qdte-lite):
qdte-lite --nogui --input_file <that uefi_dtbs.xz> --find_property QcCapsuleRootCert
  1. your python3-dtc version / oe-core revision

please check

  1. sha256sum:
    2220dcbe1dcca95d9c2de8cb8111161e56e9545cb0e2cc09e55391150ba89996 iq-x7181/spinor/uefi_dtbs.xz

Different from your 44984c3b…, which confirms the input is different.

  1. --find_property QcCapsuleRootCert:
    qcom,uefi-hamoa-1.0.dtb/sw/uefi/uefiplat/QcCapsuleRootCert
    qcom,uefi-purwa-1.0.dtbo/fragment@6/overlay/uefi/uefiplat/QcCapsuleRootCert

  2. python3-dtc / oe-core:

  • python3-dtc (libfdt): 1.8.1
  • oe-core: 237444a7dd (kas: scons-variables-revert)

@igoropaniuk

Copy link
Copy Markdown
Contributor Author

xueqnie could you provide a link to that iq-x7181/spinor/uefi_dtbs.xz?

@xueqnie

xueqnie commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

@igoropaniuk

Copy link
Copy Markdown
Contributor Author

xueqnie I found the root cause, the fix is in qualcomm/qdte-lite#4. I'll update this PR in a momemnt with this patch used in the qdte-lite recipe

The capsule root certificate lives in a device tree embedded in a boot
config ELF, so putting it there means editing a DTB inside a container
the build has no tool to open. cbsp-boot-utilities can do it, but only
through a fixed dump / set-property / replace sequence that has to be
told which DTB to touch.

qdte-lite opens these containers directly and can be asked what is in
them, which is what the following commits need to find the certificate
without hardcoding per-machine names.

Packaging it is cheap because the fork was made for this: a pyproject
with a console script, child interpreters spawned via sys.executable, and
a --nogui path that never imports a GUI toolkit. Its device-tree layer is
pylibfdt, so oe-core's python3-dtc is the only dependency and the recipe
stays out of any dynamic layer.

Carry one patch. v2.0.0 fails with FDT_ERR_NOSPACE on a tree whose
serialized size happens to be an exact multiple of the 1024-byte step
FdtSw grows its buffer in: every write method retries through
check_space(), but as_fdt() calls fdt_finish() once and gives up if the
strings block and header fixups do not fit in what is left. Nothing is
wrong with such a tree, it just lands with zero slack, so this is a size
lottery rather than a real limit and roughly one DTB in 256 loses it.
The iq-x7181-evk 00019 boot binaries draw a losing ticket, which is why
this only shows up on some machines and some firmware versions.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
The capsule recipes sat in dynamic-layers/meta-arm for one reason: they
needed edk2-basetools for GenFfs, GenFv and GenerateCapsule.py. That made
capsule generation unavailable unless a consumer also carried meta-arm,
which has nothing else to do with building a capsule.

cbsp-boot-utilities has since grown its own equivalents -- fv_builder.py
for the first two, generate-capsule for the third, byte-identical for
this subset -- so the dependency is gone and with it the only reason for
the recipes to live behind a layer that may not be present.

Bump to pick that up and move them into the normal recipe tree. meta-arm
is still needed for optee and trusted-firmware-a, so the layer stays;
only the capsule pieces leave it.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
…gned

Three things have to happen in one order and cannot be reordered: the
certificate goes into the boot config ELF, that ELF is signed, then the
capsule is built and verified against the certificate. Editing a DTB
inside the ELF invalidates any signature it already carried, so injecting
after signing produces an image the boot ROM rejects; building the
capsule before injection produces one the firmware will not authenticate.

qcom-capsule.bbclass is the wrong place for step one, because by the time
it runs the boot firmware is already deployed and whatever signs it has
already finished. There is no seam left. A separate recipe between the
boot firmware and the capsule creates one: it stages the injected ELFs
and stops, leaving do_compile and do_deploy for a signing step to sit
between.

Injection itself no longer needs the class to know anything about the
container. The DTB names are assigned during disassembly -- from
container metadata in one case, from /compatible in another -- so asking
the tool that assigns them beats configuring them per machine, which is
what XBLCONFIG_DTB and XBLCONFIG_DTB_SECTION were doing and why they go
away. Leaving the DER-to-cells conversion to bin-to-hex keeps the padding
of a trailing partial cell in one place, where the two tools cannot
disagree about it.

Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
Hamoa and similar SPINOR-boot parts have no xbl_config.elf at all: their
QcCapsuleRootCert lives in uefi_dtbs.elf, shipped xz-compressed. Without
this the injection finds nothing to patch on those machines and the
capsule is rejected at authentication time, with nothing in the build to
suggest why.

The container also holds the property more than once -- in a base DTB and
in a .dtbo overlay, at different node paths -- so anything assuming a
single target would silently patch half of it. --find_property already
reports each as its own line and --modify takes them in one pass, so
handling hamoa needs no machinery beyond staging the file.

It deploys under its own name for the same reason xbl_config does: the
boot firmware recipe owns the unmodified copy, and two recipes cannot
deploy the same filename. image_types_qcom then prefers the cert-bearing
one over the QCOM_UEFI_DTB variant, because a device flashed with the
other will not take an update.

Signed-off-by: Xueqian Nie <xueqian.nie@oss.qualcomm.com>
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
CAPSULE_FLASH_TYPE and CAPSULE_ENTRIES next to these definitions are
machine-qualified; the CAPSULE_ENTRY_dtb[...] flags beside them are not,
and cannot be -- varflags take no part in override resolution, so
CAPSULE_ENTRY_dtb[dest_disk]:iq-x7181-evk does not exist.

They therefore apply on every machine. Any other board that declares a
"dtb" capsule entry inherits hamoa's SPINOR destinations, and nothing
catches it: generate_fvupdate() only checks that an entry has a binary, a
dest_disk and a dest_partition, all of which hamoa's values supply. The
build succeeds and produces a capsule aimed at storage the machine may
not even have.

Guarding on MACHINEOVERRIDES gives the flags the scope the neighbouring
overrides already have. Renaming the entry would also work, but the class
keys the kernel dependency on the literal name "dtb".

Fixes: a314263 ("firmware-qcom-capsule: add iq-x7181-evk capsule entry definitions")
Signed-off-by: Igor Opaniuk <igor.opaniuk@oss.qualcomm.com>
@igoropaniuk

Copy link
Copy Markdown
Contributor Author

xueqnie I've pushed updated recipe with a fix, feel free to test on your side. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants